from datetime import datetime, timedelta, timezone
import json

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_db
from app.core import security
from app.core.redis_client import get_redis
from app.core.config import settings
from app.api.response import success_response
from app.models.auth_session import AuthSession
from app.models.user import User
from app.schemas import auth as auth_schema
from app.schemas import user as user_schema
import structlog

logger = structlog.get_logger('love_notes.auth')
from app.services.notification_manager import notification_manager

router = APIRouter()


@router.post('/forgot-password')
async def forgot_password(
  payload: auth_schema.ForgotPasswordRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  # 不暴露用户是否存在，始终返回 ok
  _ = await session.scalar(select(User).where(User.email == payload.email))
  try:
    redis = get_redis()
    if redis:
      import secrets
      token = secrets.token_urlsafe(32)
      await redis.set(f'auth:reset:{token}', payload.email, ex=900)
      await notification_manager.send_password_reset(email=payload.email, token=token)
  except Exception:
    pass
  return success_response({'ok': True})


@router.post('/reset-password')
async def reset_password(
  payload: auth_schema.ResetPasswordRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  redis = get_redis()
  if not redis:
    raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='重置服务未配置')
  email = await redis.get(f'auth:reset:{payload.token}')
  if not email:
    raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='重置链接无效或已过期')
  user = await session.scalar(select(User).where(User.email == email))
  if not user:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='用户不存在')
  user.password_hash = security.get_password_hash(payload.new_password)
  session.add(user)
  await session.commit()
  await redis.delete(f'auth:reset:{payload.token}')
  return success_response({'ok': True})

@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register_user(
  payload: auth_schema.RegisterRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  existing_email = await session.scalar(select(User).where(User.email == payload.email))
  existing_username = await session.scalar(select(User).where(User.username == payload.username))

  if existing_username and (not existing_email or existing_username.id != existing_email.id):
    raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在")

  if existing_email:
    if existing_email.verified_at:
      raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该邮箱已注册")
    if existing_username and existing_username.id != existing_email.id:
      raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在")

    existing_email.username = payload.username
    existing_email.password_hash = security.get_password_hash(payload.password)
    session.add(existing_email)
    await session.commit()
    await session.refresh(existing_email)
    user = existing_email
  else:
    user = User(
      email=payload.email,
      username=payload.username,
      password_hash=security.get_password_hash(payload.password),
    )
    session.add(user)
    await session.commit()
    await session.refresh(user)

  try:
    await notification_manager.send_email_verification(email=user.email, username=user.username)
  except Exception as exc:  # pragma: no cover - notification failures shouldn't block register
    logger.warning('Failed to send verification email', email=user.email, exc_info=exc)
  user_detail = user_schema.UserDetail.model_validate(user, from_attributes=True)
  return success_response(user_detail.model_dump(mode='json'))


@router.post('/resend-verification')
async def resend_verification(
  payload: auth_schema.ResendVerificationRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  user = await session.scalar(select(User).where(User.email == payload.email))
  if not user:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='用户不存在')
  if user.verified_at:
    raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='邮箱已完成验证')

  try:
    await notification_manager.send_email_verification(email=user.email, username=user.username)
  except Exception as exc:  # pragma: no cover
    logger.warning('Failed to resend verification email', email=user.email, exc_info=exc)
  return success_response({'sent': True})


@router.post("/login")
async def login_user(
  payload: auth_schema.LoginRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  if not payload.email and not payload.username:
    raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请输入邮箱或用户名")

  query = select(User)
  if payload.email:
    query = query.where(User.email == payload.email)
  else:
    query = query.where(User.username == payload.username)

  user = await session.scalar(query)
  if not user or not security.verify_password(payload.password, user.password_hash):
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账号或密码错误")

  access_token = security.create_access_token(user.id)
  refresh_token = security.create_refresh_token(user.id)

  session.add(
    AuthSession(
      user_id=user.id,
      refresh_token=refresh_token,
      expires_at=datetime.now(tz=timezone.utc)
      + timedelta(minutes=settings.refresh_token_expire_minutes),
    )
  )
  await session.commit()

  # Persist refresh token in Redis for server-side session state
  try:
    redis = get_redis()
    ttl = settings.refresh_token_expire_minutes * 60
    await redis.set(f"auth:refresh:{refresh_token}", json.dumps({"user_id": user.id}), ex=ttl)
  except Exception:
    # Redis 不可用时不阻断登录流程
    pass

  token = auth_schema.Token(
    access_token=access_token,
    refresh_token=refresh_token,
    expires_in=settings.access_token_expire_minutes * 60,
    refresh_expires_in=settings.refresh_token_expire_minutes * 60,
  )
  return success_response(token.model_dump(mode='json'))


@router.post('/verify-email')
async def verify_email(
  payload: auth_schema.VerifyEmailRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  redis = get_redis()
  if not redis:
    raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='验证码服务未配置')

  cache_key = f'auth:verify:{payload.email}'
  cached = await redis.get(cache_key)
  if not cached or cached != payload.code:
    raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='验证码错误或已过期')

  user = await session.scalar(select(User).where(User.email == payload.email))
  if not user:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='用户不存在')

  user.verified_at = datetime.now(tz=timezone.utc)
  session.add(user)
  await session.commit()
  await redis.delete(cache_key)
  return success_response({'verified': True})


@router.post("/refresh")
async def refresh_token(
  payload: auth_schema.RefreshRequest,
  session: AsyncSession = Depends(get_db),
) -> dict:
  try:
    decoded = security.decode_token(payload.refresh_token, refresh=True)
  except ValueError as exc:
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="刷新令牌无效") from exc

  # Check Redis for refresh token validity first
  redis = None
  cache_val = None
  try:
    redis = get_redis()
    cache_val = await redis.get(f"auth:refresh:{payload.refresh_token}")
  except Exception:
    cache_val = None
  if not cache_val:
    # fallback to DB (backward compatibility)
    auth_session = await session.scalar(
      select(AuthSession).where(AuthSession.refresh_token == payload.refresh_token)
    )
    if not auth_session:
      raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="刷新令牌已失效")
    expires_at = auth_session.expires_at
    if expires_at.tzinfo is None:
      expires_at = expires_at.replace(tzinfo=timezone.utc)
    if expires_at < datetime.now(tz=timezone.utc):
      raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="刷新令牌已过期")
  else:
    # Optionally validate user_id match with token subject
    try:
      info = json.loads(cache_val)
    except Exception:  # pragma: no cover
      info = None
    if not info or str(info.get("user_id")) != str(decoded.get("sub")):
      raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="刷新令牌无效")

  user_id = int(decoded["sub"])
  access_token = security.create_access_token(user_id)
  refresh_token = security.create_refresh_token(user_id)

  # Rotate Redis key (revoke old, store new)
  try:
    if redis is None:
      redis = get_redis()
    await redis.delete(f"auth:refresh:{payload.refresh_token}")
    await redis.set(
      f"auth:refresh:{refresh_token}",
      json.dumps({"user_id": user_id}),
      ex=settings.refresh_token_expire_minutes * 60,
    )
  except Exception:
    pass

  # Try to update DB session if exists
  auth_session = await session.scalar(
    select(AuthSession).where(AuthSession.refresh_token == payload.refresh_token)
  )
  if auth_session:
    auth_session.refresh_token = refresh_token
    auth_session.expires_at = datetime.now(tz=timezone.utc) + timedelta(
      minutes=settings.refresh_token_expire_minutes
    )
    session.add(auth_session)
    await session.commit()

  token = auth_schema.Token(
    access_token=access_token,
    refresh_token=refresh_token,
    expires_in=settings.access_token_expire_minutes * 60,
    refresh_expires_in=settings.refresh_token_expire_minutes * 60,
  )
  return success_response(token.model_dump(mode='json'))


@router.post("/logout")
async def revoke_refresh_token(
  payload: auth_schema.RefreshRequest,
  session: AsyncSession = Depends(get_db),
) -> None:
  try:
    redis = get_redis()
    await redis.delete(f"auth:refresh:{payload.refresh_token}")
  except Exception:
    pass
  db_session = await session.scalar(
    select(AuthSession).where(AuthSession.refresh_token == payload.refresh_token)
  )
  if db_session:
    await session.delete(db_session)
    await session.commit()
  return success_response()
